package com.ar4j;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang.builder.ToStringStyle;

import com.ar4j.util.ClassUtils;

import net.sf.cglib.proxy.Enhancer;

/**
 * Supports the existence of named factory singletons, with a default singleton used
 * if a name is not specified. 
 */
public final class NamedSingletonActiveRecordFactory implements IActiveRecordFactory {
  public static final String DEFAULT_FACTORY_NAME = "_default";
  private static final ReentrantLock FACTORY_CREATE_LOCK = new ReentrantLock();
  private static Map<String, NamedSingletonActiveRecordFactory> factoryMap = Collections.synchronizedMap(new HashMap<String, NamedSingletonActiveRecordFactory>());
  
  /**
   * @return the default factory
   */
  public static NamedSingletonActiveRecordFactory getFactory() {
    return getFactory(DEFAULT_FACTORY_NAME);
  }
  
  /**
   * @return a named factory, using the given name
   */
  public static NamedSingletonActiveRecordFactory getFactory(String name) {
    
    // get it out of the synchronized map
    NamedSingletonActiveRecordFactory out = factoryMap.get(name);
    
    // if none found, lock and try again, if its still not there, create a new instance
    if(out == null) {
      
      FACTORY_CREATE_LOCK.lock(); 
      try {
        out = factoryMap.get(name);
        if(out == null) {
          out = new NamedSingletonActiveRecordFactory(name);
          factoryMap.put(name, out);
        }
      } finally {
        FACTORY_CREATE_LOCK.unlock();
      }
    }
    
    return out;
  }
  
  private String name;
  private Map<Class<?>, ActiveRecordMetadata> metadataCache;
  private ReentrantLock cacheLock;
  
  /**
   * Disallow outside construction
   */
  private NamedSingletonActiveRecordFactory(String name) {
    this.name = name;
    this.metadataCache = new HashMap<Class<?>, ActiveRecordMetadata>();
    this.cacheLock = new ReentrantLock();
  }
  
  @Override
  @SuppressWarnings("unchecked")
  public <E extends ActiveRecord<E>> E getActiveRecord(Class<E> domainClass) {
    
    // get the meta data, cache it if needed
    ActiveRecordMetadata metadata = metadataCache.get(domainClass);
    if(metadata == null) {
      cacheLock.lock(); 
      try {
        metadata = metadataCache.get(domainClass);
        if(metadata == null) {
          metadata = ActiveRecordMetadataFactory.getMetadata(domainClass, getName());
          metadataCache.put(domainClass, metadata);
        }
      } finally {
        cacheLock.unlock();
      }
    }
    
    // build the interceptor
    DelegatingMethodInterceptor interceptor = new DelegatingMethodInterceptor(metadata, new ActiveRecordContext());
    
    // set up cglib proxy factory
    Enhancer enhancer = new Enhancer();
    enhancer.setUseFactory(true);
    enhancer.setUseCache(true);
    enhancer.setClassLoader(domainClass.getClassLoader());
    enhancer.setSuperclass(domainClass);
    enhancer.setCallback(interceptor); // single callback
    
    // create a proxy
    return (E) enhancer.create();
  }
  
  @Override
  public <E extends ActiveRecord<E>> E getActiveRecord(Class<E> domainClass, ActiveRecordJsonWrapper wrapper) {
    return getActiveRecord(wrapper.getRecord(domainClass), true);
  }
  
  @Override
  public <E extends ActiveRecord<E>> E getActiveRecord(E prototype) {
    return getActiveRecord(prototype, false);
  }
  
  @Override
  public <E extends ActiveRecord<E>> E getActiveRecord(Class<E> domainClass, Map<String, Object> values) {
    E out = getActiveRecord(domainClass);
    for(Map.Entry<String, Object> entry : values.entrySet()) {
      Class<?> requiredType = out.getPropertyType(entry.getKey());
      Object value = ClassUtils.coerceValueToType(entry.getValue(), requiredType);
      out.setProperty(entry.getKey(), value);
    }
    
    return out;
  }
  
  public String getName() {
    return name;
  }
  
  @Override
  public String toString() {
    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }
  
  /**
   * Helper for getActiveRecord which can optionally copy the ID field or not from a given
   * prototype
   */
  private <E extends ActiveRecord<E>> E getActiveRecord(E prototype, boolean copyIdField) {
    Map<String, Object> values = prototype.getPropertyMap();
    if(!copyIdField) {
      values.remove(prototype.getIdentifierPropertyName());
    }
    
    return getActiveRecord(prototype.getBaseClass(), values);
  }
}
